package com.zhanghu.backendZray.manager.minIO;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zhanghu.backendZray.cache.ContextCache;
import com.zhanghu.backendZray.constants.CommonConstant;
import com.zhanghu.backendZray.exception.ApiException;
import com.zhanghu.backendZray.manager.property.PropertyManager;
import com.zhanghu.backendZray.pojo.entity.File;
import com.zhanghu.backendZray.pojo.request.file.FileUploadRequest;
import com.zhanghu.backendZray.service.FileService;
import com.zhanghu.backendZray.utils.file.ThumbnailGenerator;
import io.minio.MinioClient;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * minIO 文件服务管理器
 *
 * @author ZhangDong
 * @date 2024/8/17 11:28
 */
@Component
@Slf4j
@RequiredArgsConstructor
@Order(2)
public class MinIOManager implements CommandLineRunner {

    private final PropertyManager propertyManager;
    private final FileService fileService;

    private static MinioClient minioClient;
    private static String bucketName;

    /**
     * 文件上传（目前只有支持图片）
     * 存放到minIO
     * 生成缩略图
     *
     * @param request
     */
    public void uploadFile(FileUploadRequest request) {
        MultipartFile multipartFile = request.getFile();
        String name = multipartFile.getOriginalFilename();
        int index = name.lastIndexOf(".");
        // 文件类型后缀
        String fileSuffix = name.substring(index);
        String previewFileName = name.substring(0, index) + CommonConstant.PREVIEW_FILE_NAME_SUFFIX + fileSuffix;
        java.io.File previewFile = null;
        String minioObjectName = null;
        String previewMinioObjectName = null;
        String md5 = null;
        Path path = null;
        String tempFileName = null;
        // 生成一个本地的临时文件
        // 因为BufferedImage originalImage = ImageIO.read(inputStream); inputStream流无法多次读取
        tempFileName = DateUtil.currentSeconds() + ThreadLocalRandom.current().nextInt(100000, 999999) + fileSuffix;
        path = Paths.get(tempFileName);
        if (!Files.exists(path)) {
            try {
                Files.createFile(path);
            } catch (IOException e) {
                log.error("file upload, 生成一个本地的临时文件失败:{}", e);
                throw new RuntimeException(e);
            }
        }
        try (InputStream inputStream = Files.newInputStream(path)) {
            multipartFile.transferTo(path);
            md5 = DigestUtils.md5DigestAsHex(inputStream);
        } catch (Exception e) {
            log.error("临时文件生成失败：{}", e);
            throw new ApiException("文件上传失败");
        }
        // 验证是否重复上传
        boolean exists = fileService.exists(new LambdaQueryWrapper<com.zhanghu.backendZray.pojo.entity.File>()
                .eq(com.zhanghu.backendZray.pojo.entity.File::getUserId, ContextCache.currentUserId())
                .eq(com.zhanghu.backendZray.pojo.entity.File::getMd5, md5));
        if (exists) {
            try {
                Files.deleteIfExists(path);
            } catch (IOException e) {
                log.error("file upload, delete temp file error:{}", e);
                throw new RuntimeException(e);
            }
            Assert.isTrue(!exists, String.format("文件《%s》 已存在，请勿重复上传", name));
        }

        // 生成缩略图
        previewFile = new java.io.File(previewFileName);
        try (InputStream inputStream = Files.newInputStream(path)) {
            ThumbnailGenerator.generateThumbnail(inputStream, previewFile, CommonConstant.PREVIEW_WIDTH, CommonConstant.PREVIEW_HEIGHT);
        } catch (IOException e) {
            log.error("缩略图生成失败：{}", e);
            throw new ApiException("文件上传失败");
        }
        try {
            // 存储文件到 存储桶中
            minioObjectName = IdUtil.fastSimpleUUID() + fileSuffix;
            previewMinioObjectName = IdUtil.fastSimpleUUID() + fileSuffix;
            minioClient.putObject(bucketName, minioObjectName, tempFileName, null);
            minioClient.putObject(bucketName, previewMinioObjectName, previewFileName, null);
            Files.deleteIfExists(Paths.get(previewFileName));
            Files.deleteIfExists(path);
        } catch (Exception e) {
            log.error("文件上传minIO失败，fileName:{}, error:{}", name, e);
            throw new ApiException("文件上传失败");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime preOneHours = now.minusHours(1);
        com.zhanghu.backendZray.pojo.entity.File file = new com.zhanghu.backendZray.pojo.entity.File();
        file.setName(name);
        file.setUserId(ContextCache.currentUserId());
        file.setMd5(md5);
        file.setSize(multipartFile.getSize());
        file.setExpireTime(preOneHours);
        file.setPreviewMinioObjectName(previewMinioObjectName);
        file.setPreviewExpireTime(preOneHours);
        file.setPreviewUrl(getFileUrl(previewMinioObjectName));
        file.setMinioObjectName(minioObjectName);
        file.setUrl(getFileUrl(minioObjectName));
        file.setExpireTime(preOneHours);
        fileService.save(file);
    }

    @SneakyThrows
    public String getFileUrl(String objectName) {
        // 不传失效时间（秒），默认7天(604800秒) "20231114-1.jpg"
        return minioClient.presignedGetObject(propertyManager.getMinIOBucketName(), objectName);
    }

    /**
     * 删除文件
     *
     * @param id
     */
    @Transactional(rollbackFor = Exception.class)
    public void delete(Long id) {
        com.zhanghu.backendZray.pojo.entity.File file = fileService.getById(id.intValue());
        Assert.notNull(file, "此文件不存在");
        String minioObjectName = file.getMinioObjectName();
        String previewMinioObjectName = file.getPreviewMinioObjectName();
        fileService.removeById(id.intValue());
        try {
            minioClient.removeObject(bucketName, minioObjectName);
            minioClient.removeObject(bucketName, previewMinioObjectName);
        } catch (Exception e) {
            log.error("删除minIO文件失败, minioObjectName:{}, previewMinioObjectName:{}, error:{}",
                    minioObjectName, previewMinioObjectName, e);
            throw new ApiException(e.getMessage());
        }
        log.info("文件删除成功，删除人id:{}, 文件id:{}", ContextCache.currentUserId(), id);
    }

    @Override
    public void run(String... args) {
        try {
            minioClient = new MinioClient(propertyManager.getMinIOEndpoint(),
                    propertyManager.getMinIOAccessKey(), propertyManager.getMinIOSecretKey());
            bucketName = propertyManager.getMinIOBucketName();
            boolean isExists = minioClient.bucketExists(bucketName);
            if (!isExists) {
                minioClient.makeBucket(bucketName);
            }
        } catch (InvalidEndpointException e) {
            log.error("minIO client init fail:{}", e);
        } catch (InvalidPortException e) {
            log.error("minIO client init fail:{}", e);
        } catch (Exception e) {
            log.error("minIO client makeBucket:{}, fail:{}", bucketName, e);
        }
    }

    /**
     * 文件链接过期处理
     */
    public void fileExpireHandle() {
        String expireHandleCount = propertyManager.getExpireHandleCount();
        Integer count = Integer.valueOf(expireHandleCount);
        Page<File> page = new Page<>(1, count);
        List<File> list = null;
        while (CollUtil.isNotEmpty(list = fileService.list(page, new LambdaQueryWrapper<File>().lt(File::getPreviewExpireTime,
                LocalDateTime.now().plusMinutes(CommonConstant.MINIO_EXPIRE_PRE_MINUTES))))) {
            fileExpireHandleDetail(list, count);
        }
        log.info("minIO文件链接过期处理 整体结束...");
    }

    public void fileExpireHandleDetail(List<File> list, int count) {
        log.info("minIO文件链接过期处理 单次开始... 一次最多处理{}条, fileList:{}", count, JSON.toJSONString(list));
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime preOneHours = now.plusDays(7).minusHours(1);
        for (File file : list) {
            file.setPreviewUrl(getFileUrl(file.getPreviewMinioObjectName()));
            file.setUrl(getFileUrl(file.getMinioObjectName()));
            file.setExpireTime(preOneHours);
            file.setPreviewExpireTime(preOneHours);
        }
        fileService.updateBatchById(list);
        log.info("minIO文件链接过期处理 单次结束... fileList:{}", JSON.toJSONString(list));
    }
}
